<?php
###########################################################################
#  Copyright (C) 2006-2007 Glyphix, Inc. <briz@glyphix.com>               #
#                                                                         #
#  Permission is hereby granted, free of charge, to any person obtaining  #
#  a copy of this software and associated documentation files (the        #
#  "Software"), to deal in the Software without restriction, including    #
#  without limitation the rights to use, copy, modify, merge, publish,    #
#  distribute, sublicense, and/or sell copies of the Software, and to     #
#  permit persons to whom the Software is furnished to do so, subject to  #
#  the following conditions:                                              #
#                                                                         #
#  The above copyright notice and this permission notice shall be         #
#  included in all copies or substantial portions of the Software.        #
#                                                                         #
#  Except as contained in this notice, the name(s) of the above           #
#  copyright holders shall not be used in advertising or otherwise to     #
#  promote the sale, use or other dealings in this Software without       #
#  prior written authorization.                                           #
#                                                                         #
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        #
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     #
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
#  IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR      #
#  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,  #
#  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#  OTHER DEALINGS IN THE SOFTWARE.                                        #
###########################################################################

/**
 * GXPayPal
 *
 * Performs PayPal Pro payments using PayPal's SOAP interface.
 * Uses some code provided by http://www.framewerk.org/ to build the SOAP message.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	Engine
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 * @uses		GXModule
 * @uses		PHP PayPal Pro 0.2.1, http://phppaypalpro.sourceforge.net/
 */

class GXPayPal extends GXModule {

	public $account;	// the name of the paypal account
	public $config;		// the config provided from System.xml
	public $apiconfig;	// the <API> node

	// paypal api settings
	public $mode;
	public $version;
	public $timeout;

	// paypal account settings
	public $username;
	public $password;
	public $signature;
	// order settings
	public $description;
	public $custom;

	// PHPPayPalPro
	public $API;
	public $transaction;

	// init GXModule and process conf
	public function __construct( $config = null ){
		parent::__construct();

		// add a transaction class
		$this->transaction = new stdClass();

		// get the settings from the provided arg
		switch(true){
			// if we don't have anything for config, get the first item
			case !$config:
				// grab the first non-API node as the default account
				$config = $this->GXPage->SystemConfig->xpath('//Modules/PayPal/*[ not(name()="API") ][1]',0)->nodeName;
				// don't break so we just continue with is_string
			// if config is a string, treat it as a paypal account name
			case is_string($config):
				$this->account = $config;
				// get the named paypal account by name
				$xpath = '//Modules/PayPal/*[ name() = "' .$this->account .'" ]';
				$this->config = $this->SystemConfig->xpath($xpath,0);
				// determine the mode
				$this->mode = $this->config->attributes['Mode'];
				// set config to the mode within the selected account
				$this->config = $this->config->exec->xpath($this->mode,0);
				break;
			// if it's an xml object, treat it as the config from SystemConfig
			case get_class($config) == 'GXDOM':
				// grab the first node as the default account
				$this->config = $config->exec->xpath('*[ not(name()="API") ][1]',0);
				$this->account = $this->config->nodeName;
				if( !$this->account ){
					trigger_error( 'Bad format in PayPal config in System.xml', E_USER_ERROR );
					return false;
				}
				// determine the mode
				$this->mode = $this->config->attributes['Mode'];
				// set config to the mode within the selected account
				$this->config = $this->config->exec->xpath($this->mode,0);
				break;
			// otherwise, we have an error
			default:
				trigger_error( 'Unable to parse config in PaymentCore constructor', E_USER_ERROR );
				return false;
		}

		// set up auth
		$this->username		= $this->config->exec->xpath('Username',0)->nodeValue;
		$this->password		= $this->config->exec->xpath('Password',0)->nodeValue;
		$this->signature	= $this->config->exec->xpath('Signature',0)->nodeValue;
		// anything else
		$this->timeout		= $this->config->exec->xpath('Timeout',0)->nodeValue;
		$this->description	= $this->config->exec->xpath('Description',0)->nodeValue;
		$this->custom		= $this->config->exec->xpath('Custom',0)->nodeValue;

		// set some constants BEFORE loading phppaypalpro
		// we'll get some PHP errors about already-defined constants, but at least we're not changing a library we're depending on
		define( 'LIVE_WSDL', 				dirname( __FILE__ ) .'/Library/phppaypalpro-0.2.1/wsdl/live/PayPalSvc.wsdl');
		define( 'SANDBOX_WSDL',				dirname( __FILE__ ) .'/Library/phppaypalpro-0.2.1/wsdl/sandbox/PayPalSvc.wsdl');
		// if our mode contains "live", true
		define( 'API_IS_LIVE',				(strtolower($this->mode) == 'live'));
		// connection timeout
		define( 'API_CONNECTION_TIMEOUT',	$this->timeout);

		// PHPPayPalPro ... hide errors
		@require_once('Library/phppaypalpro-0.2.1/paypal_base.php');
		$this->API = new WebsitePaymentsPro();
		$this->API->prepare( $this->username, $this->password, $this->signature );
		return true;
	}

	public function DoDirectPayment( $action = 'Sale', $ip = null, $merchantSessionID = null ){
		// make sure $action is valid
		$action = ucwords($action);
		if( $action != 'Sale' && $action != 'Authorization' ){
			trigger_error( 'Invalid value for action (' .$action .')', E_USER_ERROR );
		}
		// send the buyer's ip address
		if( !$ip ){
			$ip = $_SERVER['REMOTE_ADDR'];
		}

		// initialize direct payment
		$pp = $this->API->selectOperation('DoDirectPayment');

		// pointers to the main parts of the aggregator object
		$buyer = $this->transaction->buyer;
		$billing = $this->transaction->billing;
		// assume shipping address is the same as billing address if we don't have anything for billing
		$shipping = (isset($this->transaction->shipping)) ? $this->transaction->shipping : $billing;
		$cc = $this->transaction->cc;
		$payment = $this->transaction->payment;
		// sanity checks
		$msg = null;
		if( !is_array($buyer) ){
			$msg[] = 'Missing Payer Info ... call setBuyer(firstname, lastname, email, country)';
		}
		if( !is_array($billing) ){
			$msg[] = 'Missing Billing Address ... call setbilling(full_name, address1, address2, city, state, zip, country, phone)';
		}
		if( !is_array($shipping) ){
			$msg[] = 'Missing Shipping Address ... call setshipping(full_name, address1, address2, city, state, zip, country, phone)';
		}
		if( !is_array($cc) ){
			$msg[] = 'Missing Credit Card Details ... call setCreditCard(type, num, expiration_month, expiration_year, cv2)';
		}
		if( !is_array($payment) ){
			$msg[] = 'Missing Payment Details ... call setPaymentDetails(total, item_total, shipping, handling, tax, description, invoiceid)';
		}
		// if we have anything in $msg, spit out the error(s) and return false
		if( $msg ){
			trigger_error( 'Missing info: ' .implode('; ', $msg), E_USER_ERROR );
			return false;
		}

		// build the request
		$BillingAddressType = PayPalTypes::AddressType(
			$buyer['full'],
			$billing['address1'],
			$billing['address2'],
			$billing['city'],
			$billing['state'],
			$billing['zip'],
			$billing['country'],
			$buyer['phone']
		);
		$ShippingAddressType = PayPalTypes::AddressType(
			$buyer['full'],
			$shipping['address1'],
			$shipping['address2'],
			$shipping['city'],
			$shipping['state'],
			$shipping['zip'],
			$shipping['country'],
			$buyer['phone']
		);
		$PersonNameType = PayPalTypes::PersonNameType(
			$buyer['salutation'],
			$buyer['first'],
			$buyer['middle'],
			$buyer['last'],
			$buyer['suffix']
		);
		$PayerInfoType = PayPalTypes::PayerInfoType(
			$buyer['email'],
			$buyer['payerid'],
			$buyer['payerstatus'],
			$PersonNameType,
			$billing['country'],
			$buyer['business'],
			$BillingAddressType,
			$buyer['phone']
		);
		$CreditCardDetailsType = PayPalTypes::CreditCardDetailsType(
			$cc['type'],
			$cc['num'],
			$cc['expmo'],
			$cc['expyr'],
			$PayerInfoType,
			$cc['cv2']
		);
		$PaymentDetailsType = PayPalTypes::PaymentDetailsType(
			$payment['total'],
			$payment['itemtotal'],
			$payment['shipping'],
			$payment['handling'],
			$payment['tax'],
			$payment['description'],
			$payment['custom'],
			$payment['invoiceid'],
			$payment['buttonsrc'],
			$payment['notifyurl'],
			$ShippingAddressType
		);

		// build the request
		$pp->setParams( $action, $PaymentDetailsType, $CreditCardDetailsType, $ip, $merchantSessionID );
		// add items if we have them
		if( isset($this->transaction->items) && count($this->transaction->items) ){
			foreach( $this->transaction->items as $itm ){
				$pp->addPaymentItem( $itm['name'], $itm['number'], $itm['qty'], $itm['tax'], $itm['amount'], $itm['currencyid'] );
			}
		}

		// run it
		set_time_limit(0);
		$pp->execute();

		// generate the response
		$response = null;
		// pp->success() can return true even if the card didn't charge; an exception happens on soap faults and system errors
		if( $pp->success() ){
			// rebuild the success response entirely to get around some weird bug in php that results in the Amount parameter looking like this: 'Amount' => ')\000\000\000\000\000\000\000)\000\000\000\000'
			// GXDOM borks on that value
			// can't loop through the return object, since merely accessing the Amount parameter will segfault php
			$r = $pp->getAPIResponse();
			$response->Timestamp		= $r->Timestamp;
			$response->Ack				= $r->Ack;
			$response->CorrelationID	= $r->CorrelationID;
			$response->Version			= $r->Version;
			$response->Build			= $r->Build;
			$response->AVSCode			= $r->AVSCode;
			$response->CVV2Code			= $r->CVV2Code;
			$response->TransactionID	= $r->TransactionID;
			if( $r->Errors->ErrorCode ){
				$response->Errors			= new stdClass();
				$response->Errors->ShortMessage = $r->Errors->ShortMessage;
				$response->Errors->LongMessage = $r->Errors->LongMessage;
				$response->Errors->ErrorCode = $r->Errors->ErrorCode;
				$response->Errors->SeverityCode = $r->Errors->SeverityCode;
			}
		}else{
			$response = $pp->getAPIException();
			/*
			// now throw in more info about the error
			$err = new GXDOM( dirname(__FILE__) .'/Data/GXPayPal/PayPalErrors.xml' );
			$code = $response->Errors->ErrorCode;
			*/
		}

		// return it in GXDOM
		return (new GXDOM($response));
	}

	public function setBuyer( $first = null, $last = null, $email = null, $phone = null, $country = 'US' ){
		$this->transaction->buyer = array(
			'salutation'	=> null,
			'first'			=> $first,
			'middle'		=> null,
			'last'			=> $last,
			'full'			=> $first .' ' .$last,
			'suffix'		=> null,
			'email'			=> $email,
			'phone'			=> $phone,
			'country'		=> $country,
			'payerid'		=> null,
			'payerstatus'	=> null,
			'business'		=> null
		);
		return true;
	}

	public function setPayPalAccount( $payerid = null, $payerstatus = null ){
		$this->transaction->buyer['payerid'] = $payerid;
		$this->transaction->buyer['payerstatus'] = $payerstatus;
		return true;
	}

	public function setBusiness( $name = null ){
		$this->transaction->buyer['business'] = $name;
		return true;
	}

	public function setShippingAddress( $address1 = null, $address2 = null, $city = null, $state = null, $zip = null, $country = null ){
		return $this->setAddress( 'shipping', $address1, $address2, $city, $state, $zip, $country );
	}
	public function setBillingAddress( $address1 = null, $address2 = null, $city = null, $state = null, $zip = null, $country = null ){
		return $this->setAddress( 'billing', $address1, $address2, $city, $state, $zip, $country );
	}

	public function setAddress( $type, $address1 = null, $address2 = null, $city = null, $state = null, $zip = null, $country = null ){
		$this->transaction->{$type} = array(
			'address1'		=> $address1,
			'address2'		=> $address2,
			'city'			=> $city,
			'state'			=> $state,
			'zip'			=> $zip,
			'country'		=> $country
		);
		return true;
	}

	public function setCreditCard( $type = null, $num = null, $expmo = null, $expyr = null, $cv2 = null ){
		$this->transaction->cc = array(
			'type'			=> $type,
			'num'			=> preg_replace('/[^0-9]/','',$num),
			'expmo'			=> $expmo,
			'expyr'			=> $expyr,
			'cv2'			=> $cv2
		);
		return true;
	}

	public function setPaymentDetails( $total = null, $itemtotal = null, $shipping = null, $handling = null, $tax = null, $invoiceid = null, $description = null, $custom = null, $buttonsrc = null, $notifyurl = null ){
		$this->transaction->payment = array(
			'total'			=> $total,
			'itemtotal'		=> $itemtotal,
			'shipping'		=> $shipping,
			'handling'		=> $handling,
			'tax'			=> $tax,
			'invoiceid'		=> $invoiceid,
			'description'	=> ( $description ? $description : $this->description ),
			'custom'		=> ( $custom ? $custom : $this->custom ),
			'buttonsrc'		=> $buttonsrc,
			'notifyurl'		=> $notifyurl
		);
		return true;
	}

	public function addItem( $name = null, $num = null, $qty = 1, $amount = '', $tax = '', $currencyid = 'USD' ){
		$this->transaction->items[] = array(
			'name'			=> $name,
			'number'		=> $num,
			'qty'			=> $qty,
			'amount'		=> $amount,
			'tax'			=> $tax,
			'currencyid'	=> $currencyid
		);
		return true;
	}

}

?>